from helpers import *
class Value:
    def __init__(self, data, _children=(), _op='', _label=''):
        self.data = data
        self.grad = 0.0
        self._prev = set(_children)
        self._op = _op
        self._label = _label
        def default_backward():
            print(f"backward: {self._label}")
        self._backward = default_backward

    def __repr__(self):
        return f"Value(data={self.data}, label={self._label}, grad={self.grad})"

    def __add__(self, other):
        other = other if isinstance(other, Value) else Value(other)
        out = Value(self.data + other.data, (self, other), '+')
        def _backward():
            self.grad += out.grad * 1.0
            other.grad += out.grad * 1.0
            #print(f"backward: self: {self.grad}, other:{other.grad}, out:{out.grad}, label:{self._label}")
        self._backward = _backward
        return out
    
    def __radd__(self, other): # other + self
        return self + other
    
    def __sub__(self, other):
        return self + (-other)
    
    def __neg__(self):
        return self * -1
    
    def __mul__(self, other):
        other = other if isinstance(other, Value) else Value(other)
        out = Value(self.data * other.data, (self, other), '*')
        def _backward():
            self.grad += out.grad * other.data
            other.grad += out.grad * self.data
            #print(f"backward: self: {self.grad}, other:{other.grad}, out:{out.grad}, label:{self._label}")
        self._backward = _backward
        return out
    
    def __rmul__(self, other): # other * self
        return self * other
    
    def __truediv__(self, other):
        other = other if isinstance(other, Value) else Value(other)
        return self * other ** -1
    
    def __pow__(self, other):
        assert isinstance(other, (int, float)), "only supporting int/float powers for now"
        out = Value(self.data**other, (self,), f'**{other}')
        def _backward():
            self.grad += out.grad * other * self.data ** (other - 1)
        self._backward = _backward
        return out
    
    def tanh(self):
        out = Value(math.tanh(self.data), (self,), 'tanh')
        def _backward():    
            self.grad += out.grad * (1 - out.data**2)
            #print(f"backward: self: {self.grad}, out:{out.grad}, label:{self._label}")
        self._backward = _backward
        return out
    
    def exp(self):
        out = Value(math.exp(self.data), (self,), 'exp')
        def _backward():
            self.grad += out.grad * out.data
        self._backward = _backward
        return out

    def backward(self):
        self.grad = 1.0
        topo = build_topo(self)
        for v in reversed(topo):
            print(f"cur: {v.__repr__()}, {v._prev}") #v._prev is must have, or we will have some bugs
            v._backward()
            #print(f"after: {v.__repr__()}")


